# 组件化开发
就是为了封装&复用
# 使用步骤
- 创建组件构造器
- 注册组件
- 使用组件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Title</title>
</head>
<body>
<div id="app">
<!-- 3 使用组件 -->
<my-component></my-component>
<my-component></my-component>
</div>
<script src="/lib/vue.js"></script>
<script>
// 1 创建组件构造器。含义如下:
// 调用Vue.extend()创建的是一个组件构造器
// 通常在创建组件构造器时,传入template代表我们自定义的模板,该模板就是在使用到组件的地方,要显示的HTML代码
// 事实上,该写法在 Vue2.x的文档中几乎已经看不到了,它会直接使用下面要讲到的语法糖
const myComponent = Vue.extend({
template: `
<div>
<h2>标题</h2>
<p>内容1</p>
<p>内容2</p>
</div>`,
});
// 2 注册组件
Vue.component("my-component", myComponent);
const vm = new Vue({
el: "#app",
data: {
message: "Hello",
},
methods: {},
});
</script>
</body>
</html>
# 全局组件
调用 Vue.component() 注册的为全局组件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Title</title>
</head>
<body>
<div id="app">
<!-- 3 使用组件 -->
<!-- 组件必须挂载在某个 Vue 实例下,否则不生效 -->
<my-component></my-component>
<div><my-component></my-component></div>
</div>
<!-- 不生效 -->
<my-component></my-component>
<div id="app2">
<!-- 3 使用组件 -->
<!-- 组件必须挂载在某个 Vue 实例下,否则不生效 -->
<my-component></my-component>
<div><my-component></my-component></div>
</div>
<script src="/lib/vue.js"></script>
<script>
// 1 创建组件构造器。含义如下:
// 调用Vue.extend()创建的是一个组件构造器
// 通常在创建组件构造器时,传入template代表我们自定义的模板,该模板就是在使用到组件的地方,要显示的HTML代码
// 事实上,该写法在 Vue2.x的文档中几乎已经看不到了,它会直接使用下面要讲到的语法糖
const myComponent = Vue.extend({
template: `
<div>
<h2>标题</h2>
<p>内容1</p>
<p>内容2</p>
</div>`,
});
// 2 注册组件(该方式组册的为全局组件,意味着可以在多个 Vue 实例下使用)
// 调用 Vue.component() 是将刚才的组件构造器注册为一个全局组件,并给它起一个组件的标签名称
Vue.component("my-component", myComponent);
const vm = new Vue({
el: "#app",
data: {
message: "Hello",
},
});
const vm2 = new Vue({
el: "#app2",
data: {
message: "Hello",
},
});
</script>
</body>
</html>
# 局部组件 🔥
开发中一般还是更多使用局部组件
注册在某一 Vue 实例中的组件为局部组件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Title</title>
</head>
<body>
<div id="app">
<!-- 3 使用组件 -->
<!-- 组件必须挂载在某个 Vue 实例下,否则不生效 -->
<comp></comp>
<div><comp></comp></div>
</div>
<!-- 不生效 -->
<comp></comp>
<div id="app2">
<!-- 3 使用组件 -->
<!-- 组件必须挂载在某个 Vue 实例下,否则不生效 -->
<comp></comp>
<div><comp></comp></div>
</div>
<script src="/lib/vue.js"></script>
<script>
// 1 创建组件构造器。含义如下:
// 调用Vue.extend()创建的是一个组件构造器
// 通常在创建组件构造器时,传入template代表我们自定义的模板,该模板就是在使用到组件的地方,要显示的HTML代码
// 事实上,该写法在 Vue2.x的文档中几乎已经看不到了,它会直接使用下面要讲到的语法糖
const myComponent = Vue.extend({
template: `
<div>
<h2>标题</h2>
<p>内容1</p>
<p>内容2</p>
</div>`,
});
// 2 注册组件(该方式组册的为全局组件,意味着可以在多个 Vue 实例下使用)
// 调用 Vue.component() 是将刚才的组件构造器注册为一个全局组件,并给它起一个组件的标签名称
// Vue.component("my-component", myComponent);
const vm = new Vue({
el: "#app",
data: {
message: "Hello",
},
components: {
comp: myComponent,
},
});
const vm2 = new Vue({
el: "#app2",
data: {
message: "Hello",
},
});
</script>
</body>
</html>
# 父组件 & 子组件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Title</title>
</head>
<body>
<div id="app">
<!-- 3 使用组件 -->
<!-- 组件必须挂载在某个 Vue 实例下,否则不生效 -->
<my-component2></my-component2>
<!-- my-component1并没有在 app 的 Vue 实例中注册,无法使用 -->
<my-component1></my-component1>
</div>
<script src="/lib/vue.js"></script>
<script>
// 创建组件构造器1(子组件)
const myComponent1 = Vue.extend({
template: `
<div>
<h2>标题1</h2>
<p>内容1</p>
</div>`,
});
// 创建组件构造器2(父组件)
const myComponent2 = Vue.extend({
template: `
<div>
<h2>标题2</h2>
<p>内容2</p>
<my-component1></my-component1>
</div>`,
components: {
"my-component1": myComponent1,
},
});
// root 组件
const vm = new Vue({
el: "#app",
data: {
message: "Hello",
},
components: {
"my-component2": myComponent2,
},
});
</script>
</body>
</html>
# 组件名称大小写 🔥
定义组件名的方式有两种:
# 使用 kebab-case
Vue.component('my-component-name', { /* ... */ })
当使用 kebab-case (短横线分隔命名) 定义一个组件时,你也必须在引用这个自定义元素时使用 kebab-case,例如 ``。
# 使用 PascalCase 🔥
Vue.component('MyComponentName', { /* ... */ })
当使用 PascalCase (首字母大写命名) 定义一个组件时,你在引用这个自定义元素时两种命名法都可以使用。注意,尽管如此,直接在 DOM (即非字符串的模板) 中使用时只有 kebab-case 是有效的。
# 组件组册语法糖 🔥
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Title</title>
</head>
<body>
<div id="app">
<!-- 3 使用组件 -->
<!-- 组件必须挂载在某个 Vue 实例下,否则不生效 -->
<my-component1></my-component1>
<div><my-component2></my-component2></div>
</div>
<script src="/lib/vue.js"></script>
<script>
// 2 注册组件(该方式组册的为全局组件,意味着可以在多个 Vue 实例下使用)。底层还是调用Vue.extend()
// 调用 Vue.component() 是将刚才的组件构造器注册为一个全局组件,并给它起一个组件的标签名称
Vue.component("my-component1", {
template: `
<div>
<h2>标题1</h2>
<p>内容1</p>
</div>`,
});
const vm = new Vue({
el: "#app",
data: {
message: "Hello",
},
components: {
"my-component2": {
template: `
<div>
<h2>标题2</h2>
<p>内容2</p>
</div>`,
},
},
});
</script>
</body>
</html>
# 组件模板分离 🔥
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Title</title>
</head>
<body>
<div id="app">
<!-- 3 使用组件 -->
<!-- 组件必须挂载在某个 Vue 实例下,否则不生效 -->
<my-component1></my-component1>
<div><my-component2></my-component2></div>
</div>
<!-- 方式1:script标签,type="text/x-template" -->
<script type="text/x-template" id="myComponent1">
<div>
<h2>标题1</h2>
<p>内容1</p>
</div>
</script>
<!-- 方式2:template标签。推荐-->
<template id="myComponent2">
<div>
<h2>标题2</h2>
<p>内容2</p>
</div>
</template>
<script src="/lib/vue.js"></script>
<script>
// 2 注册组件(该方式组册的为全局组件,意味着可以在多个 Vue 实例下使用)。底层还是调用Vue.extend()
// 调用 Vue.component() 是将刚才的组件构造器注册为一个全局组件,并给它起一个组件的标签名称
Vue.component("my-component1", {
template: "#myComponent1",
});
const MyComponent2 = {
template: "#myComponent2",
};
const vm = new Vue({
el: "#app",
data: {
message: "Hello",
},
// components: {
// myComponent2: myComponent2,
// },
// 对象增强写法,使用时,可以使用<my-omponent2>或<MyComponent2>。注意两种命名方式使用标签时的区别
components: {
MyComponent2,
},
});
</script>
</body>
</html>
# 组件的数据 data 🔥
data 必须是一个返回组件中定义的实例对象的 function。
因为组件是用来复用的,若data是一个对象,则多个组件间对data会进行数据共享!使用 function 后每个组件都有其自己的数据,互不干扰。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Title</title>
</head>
<body>
<div id="app">
<!-- 3 使用组件 -->
<!-- 组件必须挂载在某个 Vue 实例下,否则不生效 -->
<my-component1></my-component1>
<div><my-component2></my-component2></div>
</div>
<!-- 方式1:script标签,type="text/x-template" -->
<script type="text/x-template" id="myComponent1">
<div>
<h2>{{title}}</h2>
<p>{{content}}</p>
</div>
</script>
<!-- 方式2:template标签 -->
<template id="myComponent2">
<div>
<h2>{{title}}</h2>
<p>{{content}}</p>
</div>
</template>
<script src="/lib/vue.js"></script>
<script>
// 2 注册组件(该方式组册的为全局组件,意味着可以在多个 Vue 实例下使用)。底层还是调用Vue.extend()
// 调用 Vue.component() 是将刚才的组件构造器注册为一个全局组件,并给它起一个组件的标签名称
Vue.component("my-component1", {
template: "#myComponent1",
data() {
return {
title: "标题111",
content: "内容111",
};
},
});
const myComponent2 = {
template: "#myComponent2",
data() {
return {
title: "标题222",
content: "内容222",
};
},
};
const vm = new Vue({
el: "#app",
data: {
message: "Hello",
},
components: {
"my-component2": myComponent2,
},
});
</script>
</body>
</html>
# 父子组件的通信 props & $emit 🔥
props 为 properties 缩写
场景1:父传子?比如请求后端获取的轮播图url数组,如何传递到轮播图子组件?等等。。。
场景2:子传父?比如一个页面有多个自组件构成,点击一个自组件后,父组件需要切换另一个自组件的数据,如何发事件通知?
如何进行父子组件的通信?
- 方式1:通过 props 向子组件传递数据
- 方式2:通过自定义事件 $emit 向父组件发送消息
# 父传子—props
props 的值有两种方式:
- 字符串数组:数组中的字符串就是传递时的名称
- 对象:对象可以设置传递时的类型(有类型限制),默认值等。推荐
type可以是下列原生构造函数中的一个:StringNumberBooleanArrayObjectDateFunctionSymbol
type还可以是一个自定义的构造函数,并且通过instanceof来进行检查确认- 注意驼峰命名时
v-bind的名称问题,推荐使用驼峰!
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Title</title>
</head>
<body>
<div id="app">
<my-component
v-bind:c-message="message"
:cmovies="movies"
:author="author"
:student="student"
></my-component>
<hr />
<my-component></my-component>
</div>
<template id="myComponent">
<!-- template中只能包含一个root元素 -->
<div>
<div>{{cMessage}}</div>
<ul>
<li v-for="movie in cmovies">{{movie}}</li>
</ul>
<div>{{author}}</div>
<div>{{student}}</div>
</div>
</template>
<script src="/lib/vue.js"></script>
<script>
class Person {
constructor(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
}
function Person2(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
// 大驼峰是为了方便注册组件时使用ES6对象语法
const MyComponent = {
template: "#myComponent",
data() {
return {};
},
// 1 字符串数组,不常用
// props: ["cmessage", "cmovies"],
// 2 对象,常用
// 2.1 类型限制
// props: {
// cmessage: [String, Number], // 多个类型
// cmovies: Array,
// },
// 2.2 带有默认值,必传值
props: {
// 驼峰命名时,v-bind处必须使用c-message来绑定,其他地方可以使用驼峰
cMessage: {
type: [String, Number], // 多个类型
default: "Hello MyComponent",
required: true, // 没有提供值会报错,但是还是会先显示默认值
// 自定义验证传入的值
validator(value) {
console.log(value);
// 传入的值必须为数组其中一个
return ["Hello", "World"].indexOf(value) > -1;
},
},
cmovies: {
type: Array,
// 新版本在 default 是 Object 或 Array 时,返回值必须是 function
default() {
return ["业火的向日葵", "漆黑的追踪者"];
},
required: true, // 没有提供值会报错,但是还是会先显示默认值
},
author: {
// 自定义类型
type: Person,
},
student: {
// 自定义类型
type: Person,
},
},
};
const author = new Person("san", "zhang");
const student = new Person2("si", "li");
// root 根组件
const vm = new Vue({
el: "#app",
data: {
message: "Hello",
movies: [
"引爆摩天楼",
"迷宫的十字路口",
"月光下的魔术师",
"沉默的十五分钟",
],
author,
student,
},
methods: {},
components: {
MyComponent,
},
});
</script>
</body>
</html>
# 子传父—$emit 发出自定义事件
- 在子组件中,通过
$emit('自定义事件名'[,params])发送事件,来触发子组件上绑定的自定义事件 - 在父组件中,通过
v-on来监听子组件的自定义事件,绑定到父组件的方法中 - 注意驼峰命名问题
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Title</title>
</head>
<body>
<div id="app">
<my-component @category-click="categoryClick"></my-component>
</div>
<template id="myComponent">
<!-- template中只能包含一个root元素 -->
<div>
<button v-for="category in categories" @click="btnClick(category)">
{{category.name}}
</button>
</div>
</template>
<script src="/lib/vue.js"></script>
<script>
// 子组件
// 大驼峰是为了方便注册组件时使用ES6对象语法
const MyComponent = {
template: "#myComponent",
data() {
return {
categories: [
{ id: 1, name: "热门推荐" },
{ id: 2, name: "手机数码" },
{ id: 3, name: "美妆护肤" },
{ id: 4, name: "户外健身" },
],
};
},
methods: {
btnClick(category) {
// 发出自定义事件,注意驼峰问题!!!
this.$emit("category-click", category);
},
},
};
// root 父组件
const vm = new Vue({
el: "#app",
components: {
MyComponent,
},
methods: {
categoryClick(category) {
console.log("category", category);
},
},
});
</script>
</body>
</html>
# 单向数据流 🔥
所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外变更父级组件的状态,从而导致你的应用的数据流向难以理解。
额外的,每次父级组件发生变更时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变 prop。如果你这样做了,Vue 会在浏览器的控制台中发出警告。
# v-bind & @input 实现
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Title</title>
</head>
<body>
<div id="app">
<my-component
:num1="number1"
:num2="number2"
@change-number1="changeNumber1"
@change-number2="changeNumber2"
></my-component>
</div>
<template id="myComponent">
<div>
props:{{num1}} <br />
data:{{dNum1}} <br />
props:<input type="text" v-model="num1" />不推荐直接修改<br />
data:<input
type="text"
v-model="dNum1"
/>通过data修改,但没有影响父组件中的值<br />
props:
<input
type="text"
:value="dNum1"
@input="num1Input"
/>通过$emit修改父组件中传递给子组件的值,来间接修改props值。修改
data,也可以直接绑定props的值<br />
<hr />
props:{{num2}} <br />
data:{{dNum2}} <br />
props:<input type="text" v-model="num2" />不推荐直接修改<br />
data:<input
type="text"
v-model="dNum2"
/>通过data修改,但没有影响父组件中的值<br />
props:
<input
type="text"
:value="dNum2"
@input="num2Input"
/>通过$emit修改父组件中传递给子组件的值,来间接修改props值。修改
data,也可以直接绑定props的值<br />
</div>
</template>
<script src="/lib/vue.js"></script>
<script>
const vm = new Vue({
el: "#app",
data() {
return {
number1: 1,
number2: 2,
};
},
methods: {
changeNumber1(value) {
this.number1 = Number.parseInt(value);
},
changeNumber2(value) {
this.number2 = Number.parseInt(value);
},
},
components: {
MyComponent: {
template: "#myComponent",
data() {
return {
dNum1: this.num1,
dNum2: this.num2,
};
},
props: {
num1: {
type: Number,
default: 1,
required: true,
},
num2: {
type: Number,
default: 2,
required: true,
},
},
methods: {
num1Input(event) {
console.log(event.target.value);
this.$emit("change-number1", event.target.value);
// 注意,子组件中 data 数据不会随父组件更新了自组件的 props 后更改
this.dNum1 = event.target.value;
},
num2Input(event) {
console.log(event.target.value);
this.$emit("change-number2", event.target.value);
// 注意,子组件中 data 数据不会随父组件更新了自组件的 props 后更改
this.dNum2 = event.target.value;
},
},
},
},
});
</script>
</body>
</html>
每次直接修改 props 中数据控制台就会报错
[Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "num1"
# watch 实现
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Title</title>
</head>
<body>
<div id="app">
<my-component
:num1="number1"
:num2="number2"
@change-number1="changeNumber1"
@change-number2="changeNumber2"
></my-component>
</div>
<template id="myComponent">
<div>
props:{{num1}} <br />
data:{{dNum1}} <br />
props:<input type="text" v-model="num1" />不推荐直接修改<br />
data:<input
type="text"
v-model="dNum1"
/>通过data修改,但没有影响父组件中的值<br />
props:
<input
type="text"
v-model="dNum1"
/>通过$emit修改父组件中传递给子组件的值,来间接修改props值。修改
data,也可以直接绑定props的值<br />
<hr />
props:{{num2}} <br />
data:{{dNum2}} <br />
props:<input type="text" v-model="num2" />不推荐直接修改<br />
data:<input
type="text"
v-model="dNum2"
/>通过data修改,但没有影响父组件中的值<br />
props:
<input
type="text"
v-model="dNum2"
/>通过$emit修改父组件中传递给子组件的值,来间接修改props值。修改
data,也可以直接绑定props的值<br />
</div>
</template>
<script src="/lib/vue.js"></script>
<script>
const vm = new Vue({
el: "#app",
data() {
return {
number1: 1,
number2: 2,
};
},
methods: {
changeNumber1(value) {
this.number1 = Number.parseInt(value);
},
changeNumber2(value) {
this.number2 = Number.parseInt(value);
},
},
components: {
MyComponent: {
template: "#myComponent",
data() {
return {
dNum1: this.num1,
dNum2: this.num2,
};
},
props: {
num1: {
type: Number,
default: 1,
required: true,
},
num2: {
type: Number,
default: 2,
required: true,
},
},
// watch只能监听data中数据
watch: {
dNum1(newValue, oldValue) {
this.$emit("change-number1", newValue);
},
dNum2(newValue) {
this.$emit("change-number2", newValue);
},
},
},
},
});
</script>
</body>
</html>
# 父子组件的访问—$refs 🔥
基本上只有 $refs会使用很多,其他的基本不使用
# 父访问子—$children & $refs 🔥
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Title</title>
</head>
<body>
<div id="app">
<my-component ref="a"></my-component>
<my-component></my-component>
<button @click="btnClick">父组件按钮</button>
</div>
<template id="myComponent">
<div>我是子组件</div>
</template>
<script src="/lib/vue.js"></script>
<script>
const vm = new Vue({
el: "#app",
data: {
message: "Hello",
},
methods: {
btnClick() {
// $children,是数组,使用很少
// console.log(this.$children);
// this.$children.forEach((child) => {
// child.showMessage();
// console.log(child.message);
// });
// $refs,是对象,默认为空,只有有ref属性的才会放入该对象中
console.log(this.$refs.a);
console.log(this.$refs["a"]);
this.$refs.a.showMessage();
console.log(this.$refs.a.message);
},
},
components: {
MyComponent: {
template: "#myComponent",
data() {
return {
message: "Hello",
};
},
methods: {
showMessage() {
console.log("showMessage");
},
},
},
},
});
</script>
</body>
</html>
# 子访问父—$parent & $root
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Title</title>
</head>
<body>
<div id="app">
<outer-component ref="a"></outer-component>
</div>
<template id="outerComponent">
<div>
<inner-component></inner-component>
</div>
</template>
<template id="innerComponent">
<div>
<div>我是子组件</div>
<button @click="btnClick">子组件按钮</button>
</div>
</template>
<script src="/lib/vue.js"></script>
<script>
const vm = new Vue({
el: "#app",
data: {
message: "Hello Vue",
},
components: {
OuterComponent: {
template: "#outerComponent",
data() {
return {
message: "Hello OuterComponent",
};
},
components: {
innerComponent: {
template: "#innerComponent",
methods: {
btnClick() {
// 打印出的为 VueComponent。基本不会使用
console.log(this.$parent);
console.log(this.$parent.message);
// 打印出的为 Vue。基本不会使用,以后 Vue 实例中仅仅会放 Router 等,不会放数据,方法等
console.log(this.$root);
console.log(this.$root.message);
},
},
},
},
},
},
});
</script>
</body>
</html>